/**
 * Copyright (c) 2010-2011 Pierre LABATUT
 *
 * This file is part of multiple-track-player project.
 *
 * multiple-track-player is free software: you can redistribute it and/or modify it
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or <http://code.google.com/p/multiple-track-player/>.
 */
package multitrack.preload;

import java.util.Arrays;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioFormat.Encoding;

/**
 * Resizable buffer that accept data to be appended and data to be read concurrently
 * by multiple threads.
 * The buffer contains interleaved signed PCM data for each audio channel.
 * 
 * This intrinsic lock is bound to a condition queue involving 'isComplete' and 
 * 'limit'.
 * 
 * @author Pierre LABATUT
 */
public class HeapAudioBuffer implements AudioBuffer {

    private boolean isComplete = false;
    private final int allocChunkSize = 1 << 19;
    // memory area where is stored 16 bit signed PCM
    private short[] buffer = new short[allocChunkSize * 16];
    // [0,limit[ range of the buffer contains data
    private int limit = 0;
    // buffer content format. (Must be 16 bit signed PCM)
    private final AudioFormat format;

    public HeapAudioBuffer(final AudioFormat format) {

        if (format == null) {
            throw new IllegalArgumentException("Formant can't be null");
        }
        this.format = format;

        if (format.getEncoding().equals(Encoding.PCM_SIGNED) == false) {
            throw new IllegalArgumentException("Encoding must be signed PCM");
        }
        if (format.getSampleSizeInBits() != Short.SIZE) {
            throw new IllegalArgumentException("Sample must be 16 bit");
        }
    }
    
    public ShortWriter getWriter() {
        return new ShortWriter() {

            @Override
            public void write(short[] sb, int offset, int length) {
                if (sb == null) {
                    throw new IllegalArgumentException();
                }
                synchronized (HeapAudioBuffer.this) {
                    if (isComplete == true) {
                        throw new IllegalStateException("Cannot append data to a complete buffer");
                    }
                    //Get the fresh data length

                    //Resize de buffer if needed
                    if (limit + length > buffer.length) {
                        int allocChunkCount = ((limit + length - 1) / allocChunkSize) + 1;
                        buffer = Arrays.copyOf(buffer, allocChunkCount * allocChunkSize);
                    }
                    //Write the new data at the end of the buffer
                    System.arraycopy(sb, offset, buffer, limit, length);
                    limit += length;
                    //Notify all wauters that data new data is available
                    //this notify is related with a condition predicate on limit
                    HeapAudioBuffer.this.notifyAll();
                }
            }

            @Override
            public void close() {
                synchronized (HeapAudioBuffer.this) {
                    if (isComplete == true) {
                        throw new IllegalStateException("Cannot append data to a complete buffer");
                    }
                    isComplete = true;
                    HeapAudioBuffer.this.notifyAll();
                }
            }
        };
    }


    @Override
    public int read(int fromSample, int sampleCount, short[] target) throws InterruptedException {
        if (fromSample < 0) {
            throw new IllegalArgumentException("Negative position");
        }
        if (sampleCount <= 0) {
            throw new IllegalArgumentException("Negative sample count");
        }

        synchronized (this) {
            waitAvailableOrComplete(fromSample + sampleCount);

            if (fromSample >= limit) {
                return -1;
            }

            fromSample = Math.min(limit, fromSample);//Do not go after the limit
            sampleCount = Math.min(sampleCount, limit - fromSample);
            System.arraycopy(buffer, fromSample, target, 0, sampleCount);
            return sampleCount;
        }
    }

    @Override
    public AudioFormat format() {
        return format;
    }

    @Override
    public synchronized int samples() {
        return limit;
    }
    
    public void minmax(int fromSample, int toSample, short minmax[]) throws InterruptedException {

        synchronized (this) {
            waitAvailableOrComplete(toSample);
            toSample = Math.min(limit, toSample);
        }
        fromSample = Math.max(0, fromSample);
        short min = minmax[0];
        short max = minmax[1];
        for (int i = fromSample; i < toSample; i++) {
            if (min < buffer[i]) {
                min = buffer[i];
            }
            if (max > buffer[i]) {
                max = buffer[i];
            }
        }
        minmax[0] = min;
        minmax[1] = max;
    }


    synchronized void waitAvailableOrComplete(int sampleCount) throws InterruptedException {
        while (isComplete == false && limit < sampleCount) {
            wait();
        }
    }

    synchronized boolean isCompleteWith(int sampleCount) throws InterruptedException {
        while (isComplete == false && limit <= sampleCount) {
            wait();
        }
        return isComplete && limit == sampleCount;
    }
}
